﻿using System;
using System.Data;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Collections;

namespace AxeFrog.Data.Expressions
{
	public class SqlExpressionVisitor : ExpressionVisitor
	{
		private IDbCommand _cmd;
		private StringBuilder _sb;

		public SqlExpressionVisitor(IDbCommand cmd)
		{
			_cmd = cmd;
		}

		public string Translate(Expression expr)
		{
			_sb = new StringBuilder();
			var pexpr = Evaluator.PartialEval(expr);
			Visit(pexpr);
			return _sb.ToString();
		}

		protected override Expression VisitUnary(UnaryExpression u)
		{
			switch(u.NodeType)
			{
				case ExpressionType.Not:
					_sb.Append(" NOT (");
					VisitBooleanExpression(u.Operand);
					_sb.Append(") ");
					break;
				default:
					throw new NotSupportedException(string.Format("The unary operator '{0}' is not supported", u.NodeType));
			}
			return u;
		}

		private Expression VisitBinarySubExpression(BinaryExpression parentExpression, Expression subExpression)
		{
			switch(parentExpression.NodeType)
			{
				case ExpressionType.Equal:
				case ExpressionType.NotEqual:
				case ExpressionType.LessThan:
				case ExpressionType.LessThanOrEqual:
				case ExpressionType.GreaterThan:
				case ExpressionType.GreaterThanOrEqual:
					break;
				default:
					return VisitBooleanExpression(subExpression);
					break;
			}
			return Visit(subExpression);
		}

		private Expression VisitBooleanExpression(Expression expr)
		{
			if(expr is ConstantExpression)
			{
				var v = ((ConstantExpression)expr).Value;
				if(v is bool)
				{
					_sb.Append("1 = ").Append((bool)v ? 1 : 0);
					return expr;
				}
			}
			else if(expr is MemberExpression)
			{
				var m = (MemberExpression)expr;
				Type t;
				if(m.Member is PropertyInfo)
					t = ((PropertyInfo)m.Member).PropertyType;
				else if(m.Member is FieldInfo)
					t = ((FieldInfo)m.Member).FieldType;
				else
					return Visit(expr);
				if(!t.Equals(typeof(bool)))
					return expr;
				var e = Visit(expr);
				_sb.Append(" = 1");
				return e;
			}
			return Visit(expr);
		}

		protected override Expression VisitBinary(BinaryExpression b)
		{
			_sb.Append("(");
			VisitBinarySubExpression(b, b.Left);
			switch(b.NodeType)
			{
				case ExpressionType.AndAlso:
					_sb.Append(" AND ");
					break;
				case ExpressionType.OrElse:
					_sb.Append(" OR ");
					break;
				case ExpressionType.Equal:
					if(b.Right.NodeType == ExpressionType.Constant && ((ConstantExpression)b.Right).Value == null)
						_sb.Append(" IS ");
					else
						_sb.Append(" = ");
					break;
				case ExpressionType.NotEqual:
					if(b.Right.NodeType == ExpressionType.Constant && ((ConstantExpression)b.Right).Value == null)
						_sb.Append(" IS NOT ");
					else
						_sb.Append(" <> ");
					break;
				case ExpressionType.LessThan:
					_sb.Append(" < ");
					break;
				case ExpressionType.LessThanOrEqual:
					_sb.Append(" <= ");
					break;
				case ExpressionType.GreaterThan:
					_sb.Append(" > ");
					break;
				case ExpressionType.GreaterThanOrEqual:
					_sb.Append(" >= ");
					break;
				default:
					throw new NotSupportedException(string.Format("The binary operator '{0}' is not supported", b.NodeType));
			}
			VisitBinarySubExpression(b, b.Right);
			_sb.Append(")");
			return b;
		}

		protected override Expression VisitConstant(ConstantExpression c)
		{
			WriteParameter(c.Value);
			return c;
		}

		private void WriteParameter(object value)
		{
			if(value == null)
				_sb.Append("NULL");
			else
			{
				_sb.Append("@Prm" + _cmd.Parameters.Count);
				_cmd.AddParameter("@Prm" + _cmd.Parameters.Count, value, value.GetType().ToDbType());
			}
		}

		protected override Expression VisitMemberAccess(MemberExpression m)
		{
			var exp = base.VisitMemberAccess(m);
			_sb.Append(m.Member.Name);
			return m;
		}

		protected override Expression VisitMethodCall(MethodCallExpression m)
		{
			if(m.Method.Name == "Contains")
			{
				IEnumerable list = null;
				Expression expr = null;
				if(typeof(IEnumerable).IsAssignableFrom(m.Method.DeclaringType) && m.Object is ConstantExpression)
				{
					list = (IEnumerable)((ConstantExpression)m.Object).Value;
					expr = m.Arguments[0];
				}
				else if(m.Arguments.Count == 2 && m.Arguments[0] is ConstantExpression)
				{
					list = ((ConstantExpression)m.Arguments[0]).Value as IEnumerable;
					expr = m.Arguments[1];
				}
				if(list != null)
				{
						// if the list is empty, the condition will always evaluate to false
						if(!list.GetEnumerator().MoveNext())
						{
							// write a proxy false condition seeing as we can't use an IN operator with no arguments
							_sb.Append("(1 = 0)");
							return m;
						}
						_sb.Append("(");
						Visit(expr);
						_sb.Append(" IN (");
						int c = 0;
						foreach(object obj in list)
						{
							if(c++ > 0)
								_sb.Append(", ");
							WriteParameter(obj);
						}
						_sb.Append("))");
						return m;
				}
			}
			return base.VisitMethodCall(m);
		}
	}
}